feat(db): add experimental db tunnel (MySQL over WebSocket) - #249
Merged
Merged
Conversation
Add `gddy db tunnel`, an experimental streaming command that opens a local TCP port and bridges raw MySQL bytes over a single WebSocket per connection to an application's agent, which dials the app's own database. The tunnel is a byte pump: it never parses MySQL and never injects credentials, so MySQL auth and TLS are negotiated end-to-end between the client and the database. The command authenticates the one-time token mint with the CLI's own OAuth credential, receives a short-lived app-scoped token and the agent URL from the hosting API, and presents that token to the agent. The `db` module is gated behind the experimental stage and hidden at the GA default. Includes the public proposal at docs/proposals/db-tunnel.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
The WebSocket URL builder can generate invalid URLs for IPv6 agent hosts (missing required brackets), which can break connections in valid environments.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds an experimental gddy db tunnel command group for tunneling MySQL traffic from a local TCP listener to a hosted app’s agent via per-connection WebSockets, plus the hosting API client support needed to mint short-lived agent tokens.
Changes:
- Registers a new experimental
dbcommand group and adds CLI-stage gating tests. - Implements
db tunnelas a streaming command that relays raw bytes between TCP and WebSocket connections. - Extends the hosting Node.js client with an
agent-tokenmint endpoint and associated unit test; adds required Rust dependencies and a design proposal doc.
File summaries
| File | Description |
|---|---|
| rust/src/main.rs | Wires in the db module and adds a gating/help-surface test for db tunnel. |
| rust/src/hosting/nodejs/client.rs | Adds get_agent_token API call and a unit test for request/response shape. |
| rust/src/db/tunnel.rs | Implements the streaming tunnel command, URL derivation, token minting, and byte relay. |
| rust/src/db/mod.rs | Adds the experimental db group wiring and registers db tunnel. |
| rust/Cargo.toml | Adds futures-util and tokio-tungstenite dependencies for WS + stream utilities. |
| rust/Cargo.lock | Locks new transitive dependencies for tungstenite/rustls stack. |
| docs/proposals/db-tunnel.md | Documents the design, security model, and rationale for db tunnel. |
Review details
- Files reviewed: 6/7 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
jpage-godaddy
approved these changes
Sep 4, 2026
jpage-godaddy
left a comment
Collaborator
There was a problem hiding this comment.
This looks great.
- Refuse to bind a non-loopback interface unless the operator passes the new --allow-non-loopback flag. The local port relays straight to the app's live database with no authentication of its own, so it must not be put on the network by accident. Fail closed before a token is minted, and treat any host we cannot prove is loopback -- including a bare hostname we would have to resolve -- as non-loopback. - Emit an explicit warning that the tunnel targets the app's live database, and a second warning whenever a non-loopback bind is actually in effect. - Withhold secret-bearing response bodies from the `--debug transport` trace. The agent-token response carries a bearer token, and the transport logger prints response bodies verbatim (only sensitive headers are redacted), so route that one call through a body-suppressing path. A minted token can no longer reach the debug output; status and headers still log. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… to TLS The experimental `db tunnel` command now steps its OAuth credential up to a dedicated hosting.database:tunnel scope in addition to hosting.paas.deploy:execute, so authority to publish a deployment does not by itself grant raw database read/write. The scope is declared in the registry (default off) and presented at the mint call; the edge enforces both scopes. Because the tunnel forwards bytes without terminating TLS, the command's long help, its connect hint, and the proposal now steer operators to connect their MySQL client with TLS enabled (for example --ssl-mode=REQUIRED) so the local hop is encrypted as well, and note the database may compel it with require_secure_transport=ON. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mcolakovic-godaddy
requested review from
axburgess-godaddy,
jhenderson1-godaddy,
jpearlman-godaddy,
mguerrero3-godaddy,
qcai-godaddy and
rts1-godaddy
as code owners
September 18, 2026 11:29
Reconcile the experimental db-tunnel work with main's hosting module reorg: - main.rs: keep `mod db;` / `db::module()` alongside the renamed `api` module and drop the now-folded `contacts` module. - scopes.rs: re-apply HOSTING_DATABASE_TUNNEL on top of main's renamed hosting scope constants and SCOPE_REGISTRY entries. - hosting/client.rs + client_tests.rs: carry `get_agent_token` and its secret-response redaction onto main's flattened `/v1/hosting` client; the mint keeps its explicit `/nodejs` path segment. - db/tunnel.rs: retarget imports at the relocated `crate::http` and `crate::hosting::client`. - Remove the orphaned `hosting/nodejs` subtree deleted on main. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
build_tunnel_ws_url mapped `http|ws` to plain `ws`. Nothing can reach that branch any more, and it is a latent cleartext-downgrade path, so drop it and admit only `https|wss`. The agent URL is service-supplied: it comes only from the mint response (`app.urls.agent`, read verbatim from mgmt-airo), and `--agent-url` was removed before this shipped. In production the data plane is CLI -> Cloudflare (TLS) -> v2-ingress-proxy -> agent `:80`, where the plaintext listener sits inside the cell and is reachable from ingress nodes only; the host in `agentUrl` is always the Cloudflare-fronted https name. The one consumer of the `http` branch was agents/scripts/db-tunnel-local.ts, a throwaway harness since deleted from airo-app-builder. Nothing validates the scheme upstream either -- the mint response schema is `agentUrl: z.string()`, trimmed and checked non-empty only -- so a mis-provisioned or tampered record would have silently put the agent JWT and every MySQL byte on the wire in the clear (CWE-319). Rejecting instead fails the run with a clear error before any port is bound. It also makes the CLI's `rustls-tls-webpki-roots` posture unconditional: a vendored root set does nothing when the scheme is not `wss` at all. The `https`->`wss` mapping itself has to stay -- tungstenite's `into_client_request` accepts only `ws`/`wss`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Conflict in rust/src/hosting/client.rs: main (godaddy#276, godaddy#278) replaced the hand-rolled request helpers with the generated `hosting-client` crate, while this branch had added `send_json_secret_response` -- a variant that withholds the response body from the `--debug transport` trace so the agent-token mint cannot log a minted bearer token (DBT-10). Resolution keeps main's refactor and preserves that redaction: - Dropped `send_json` / `send_json_secret_response` / `send_json_inner`. After the refactor every caller goes through `api()`, `send_patch` or `post_empty_json`, so they had no remaining users. - Kept the `REDACTED_RESPONSE_BODY` marker and moved the redaction onto `post_empty_json`, which is the helper `get_agent_token` now needs: split into `post_empty_json_inner(path, log_response_body)` behind `post_empty_json` and `post_empty_json_secret_response`. `create_deployment` keeps the logging variant; only the mint redacts. - `get_agent_token` now calls `post_empty_json_secret_response`, preserving its wire contract (POST, empty JSON body, bearer auth) -- pinned by the unchanged `get_agent_token_posts_empty_body_and_returns_url_and_token` test, which still passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jpage-godaddy
approved these changes
Sep 18, 2026
jpage-godaddy
requested changes
Sep 18, 2026
The `db tunnel` command declared `hosting.database:tunnel`, which was never a real, registered OAuth scope. Rename the constant and value to `hosting.database.tunnel:execute` — the scope now registered in AuthZ — so the CLI requests a grantable scope at OAuth step-up. This is a dedicated grant, separate from `hosting.deployment:execute`: authority to publish a deployment does not by itself grant raw database read/write, so opening a MySQL-over-WebSocket tunnel needs its own scope. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jpage-godaddy
approved these changes
Sep 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
gddy db tunnel, an experimental streaming command that opens a local TCP port and bridges raw MySQL bytes over a single WebSocket per connection to an application's agent, which in turn dials the application's own database. This lets a developer point any local MySQL client (mysql, TablePlus, DataGrip, an ORM) at a hosted app's database without exposing that database publicly.Full design and rationale:
docs/proposals/db-tunnel.md.How it works
dbgroup is registered behind the experimental stage and hidden at the GA default, so it does not appear in normal help output.Security
Testing
cargo fmt --check,cargo clippy -- -D warnings,cargo test(757 tests),cargo check --locked, and the module-size check all pass.Notes